package com.llbnk.service.impl;

import com.llbnk.dao.OrderDOMapper;
import com.llbnk.dao.SequenceDOMapper;
import com.llbnk.dao.StockLogDOMapper;
import com.llbnk.error.BusinessException;
import com.llbnk.error.EmBusinessError;
import com.llbnk.pojo.entity.OrderDO;
import com.llbnk.pojo.entity.SequenceDO;
import com.llbnk.pojo.entity.StockLogDO;
import com.llbnk.pojo.model.ItemModel;
import com.llbnk.pojo.model.OrderModel;
import com.llbnk.pojo.model.UserModel;
import com.llbnk.service.OrderService;
import org.springframework.beans.BeanUtils;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.transaction.support.TransactionSynchronizationAdapter;
import org.springframework.transaction.support.TransactionSynchronizationManager;

import javax.annotation.Resource;
import java.math.BigDecimal;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;

/**
 * @author llbnk
 * @create 2022-08-21 22:01
 */
@Service
public class OrderServiceImpl implements OrderService {

    @Resource
    private ItemServiceImpl itemService;

    @Resource
    private OrderDOMapper orderDOMapper;

    @Resource
    private UserServiceImpl userService;

    @Resource
    private SequenceDOMapper sequenceDOMapper;

    @Resource
    private StockLogDOMapper stockLogDOMapper;

    //为了提供提交完在进行某一个操作


    //创建订单代码过于臃肿需要拆分解耦
    @Override
    /**
     * Transactional只有等到所有操作执行完才会提交.
     * 如果在第四步更新
     * 4.异步更新库存的时候，需要在订单不出错的情况下执行
     * toAsynDecreaseStock(itemId,amount);
     * 还是有可能会在最后返回orderModel的时候因为网络问题，或者磁盘满了，还是出现了少卖情况。
     * Transactional还提供给我们，先提交完然后在进行异步更新库存的操作。
     */
    @Transactional
    ////promoId非空表示已预约活动价格下单
    public OrderModel createOrder(Integer userId, Integer itemId, Integer promoId,
                                  Integer amount,String stockLogId) throws BusinessException {
        /**
         * 1.校验下单状态下单商品是否存在，用户是否合法，购买数量是否正确
         * 2.采取落单减库存（一般电商分为两种一种是落单减库存，支付减库存）
         * 落单减库存就是用户先落单了，此时系统就去锁库存了。也就是再支付之前用户就相当于拥有了这个商品。
         * 而支付减库存，就是落单了，系统检测还有库存，但是用户在支付完后，再次系统去锁库存，发现已经没有
         * 库存了只能给用户退款了。
         * 3.订单入库
         * 4.返回前端
         */

        //1.校验下单状态，商品是否存在，用户是否合法，购买数量是否正确
        /**因为，后面需要使用秒杀令牌的方式，再PromoService的generateSecondKillToken
         * 方法中不但对用户，商品，秒杀活动做验证，还生成了秒杀令牌token，所以
         * 这里就不需要校验下单了*/
        //verifyingUserInformation(itemId,userId,amount,promoId);
        checkAmountNumber(amount);


        //2.落单减库存，支付减库存
        orderDecreaseStock(itemId, amount);

        //自己添加，应对订单出现错误的时候，回滚第二步redis扣减的订单，防止噪声少买
        //注意必须放在订单前面订单失败才能回滚
        toAsynIncreaseRedisStock(itemId,amount);

        //3.订单入库
        OrderModel orderModel = orderintoMysql(itemId, userId, amount, promoId);


        //4.因为采用了MQ事务发送机制，而这个订单成功的时间是不确定的，使用MQ事务有事务补偿机制
        //所以在询问本地事务的时候返回是需要通过流水stock_log的status来判断的,当来到这里的时候说明订单都已经完成了
        //所以此时我们应该将stock_log中的status设置为成功状态
        //(1)问题又来了，当前不是又插入数据库了岂不是并发性又下降了?
        //这种插入数据库是根据stockLogId进行插入的每一笔订单都有自己的stockLogId，而会在stockLogId上添加行锁
        //这个行锁不存在并发关系，而在stock表中大家订单都需要取更改库存这个操作就是并发的，所以相当于将并发改数据库
        //变成了非并发更改流水表中的字段
        //(2)问题又来了，假如说我们当前商品只有100个，但我们有1w个人点击了在OrderController中
        //设置插入数据库，此时就会造成生成了1w个流水信息，产生了极大的浪费
        stockLogToSuccess(stockLogId);


        /**
         *这个地方的逻辑是首先在扣减redis中的库存然后不着急更新数据库
         * 等到订单模块生成无误之后，在进行异步更新redis和数据库中的数据同步
         * 如果此时异步更新数据库失败了再调用异步increaseStock加回来redis中数据即可（因为数据库有@Transactional）
         */
        //4.异步更新库存的时候，需要在订单不出错的情况下执行
        //当采用rocketMQ的事务时抛弃这个方法
        //toAsynDecreaseStock(itemId,amount);

        //5.返回前端
        return orderModel;
    }

    @Transactional
    public void toAsynIncreaseRedisStock(Integer itemId, Integer amount) throws BusinessException{
        TransactionSynchronizationManager
                .registerSynchronization(new TransactionSynchronizationAdapter() {
            @Override
            public void afterCompletion(int status) {
                //参考连接：https://www.cnblogs.com/ciel717/p/16190723.html
                /**status 0：外部方法事务提交后执行的业务逻辑
                 *        1：外部方法事务回滚后执行的业务逻辑
                 *        2：外部方法事务异常时执行的业务逻辑
                 */
                if(status!=0){
                    itemService.asynchronousIncreaseStock(itemId,amount);
                }
            }
        });

    }

    private void stockLogToSuccess(String stockLogId) throws BusinessException {
        StockLogDO stockLogDO = stockLogDOMapper.selectByPrimaryKey(stockLogId);
        if(stockLogDO == null){
            throw new BusinessException(EmBusinessError.UNKNOW_ERROR);
        }
        //如果能拿到这个订单表的字段就将status更改为2
        stockLogDO.setStatus(2);
        stockLogDOMapper.updateByPrimaryKeySelective(stockLogDO);
    }

    //当采用rocketMQ的事务时抛弃这个方法
    /**负责createOrder中的orderDecreaseStock的异步更新数据库库存操作*/
    private void toAsynDecreaseStock(Integer itemId, Integer amount) throws BusinessException {
        // TransactionSynchronizationManager.registerSynchronization意思是最近的Transactional提交后会触发这个
        TransactionSynchronizationManager
                .registerSynchronization(new TransactionSynchronizationAdapter() {
            @Override
            public void afterCommit() {
                //异步更新库存
                boolean mqResult = itemService.asyncDecreaseStock(itemId, amount);
                //使用TransactionSynchronizationManager在createOrder业务做完在进行更新数据库操作
                //必须需要mq发送必须成功才行如果失败了下面是无法回滚的
                //问题来了为什么下面回滚是毫无意义的？
                //因为你已经生成订单了，就证明本次下单成功了，此时已经在order_info中生成了你这个订单了
                //你回滚顶多价格销量订单成功已经返回给前端了

                /**
                 * 现在只有一个问题了，如何保证MQ发送必定成功？这就需要用到rocketmq事务性消息
                 */

//                if (!mqResult) {
//                    itemService.increaseStock(itemId, amount);
//                    throw new BusinessException(EmBusinessError.MQ_SEND_FAIL);
//                }
            }
        });
    }


    /**负责createOrder中的订单入库操作*/
    private OrderModel orderintoMysql(Integer itemId, Integer userId, Integer amount, Integer promoId) {
        ItemModel itemModel = itemService.getItemById(itemId);
        OrderModel orderModel = new OrderModel();
        orderModel.setItemId(itemId);
        orderModel.setUserId(userId);
        orderModel.setAmount(amount);
        orderModel.setItemPrice(itemModel.getPrice());
       //System.out.println(10/0);
        if (promoId != null) {
            //商品价格取特价
            orderModel.setItemPrice(itemModel.getPromoModel().getPromoItemPrice());
        } else {
            orderModel.setItemPrice(itemModel.getPrice());
        }
        orderModel.setPromoId(promoId);
        orderModel.setOrderPrice(orderModel.getItemPrice().multiply(new BigDecimal(amount)));

        //生成交易流水号
        orderModel.setId(generateOrderNo());

        OrderDO orderDO = convertFromOrderModel(orderModel);
        orderDOMapper.insertSelective(orderDO);

        //加上商品的销量
        itemService.increaseSales(itemId, amount);
        return orderModel;
    }

    /**负责createOrder中的减库存操作*/
    private void orderDecreaseStock(Integer itemId, Integer amount) throws BusinessException {
        //boolean result = itemService.decreaseStock(itemId, amount);
        //采用异步方式更新库存 保证订单生成没问题才会去插入数据库
        //boolean result = itemService.asynchronousDecreaseStock(itemId, amount);
        
        //采用带有售空标志的异步扣库存方式，这样就能保证大量生成订单流水的浪费
        boolean result = itemService.SoldOutAsynDecreaseStock(itemId, amount);
        if (!result) {
            throw new BusinessException(EmBusinessError.STOCK_NOT_ENOUGH);
        }
    }



    /**因仅有秒杀令牌的生成，此时不需要再次检验用户订单秒杀活动信息，只需要检验下单参数是否合法就可以了*/
    private void checkAmountNumber(Integer amount) throws BusinessException {
        if (amount <= 0 || amount > 99) {
            throw new BusinessException(EmBusinessError.PARAMETER_VALIDATION_ERROR, "数量信息不正确");
        }
    }

    /**负责createOrder中的用户校验工作*/
    private void verifyingUserInformation(Integer itemId, Integer userId,
                                          Integer amount,Integer promoId) throws BusinessException {
        //这一步是没有缓存的直接查mysql    ItemModel itemModel = itemService.getItemById(itemId);
        //这一步是走缓存的
        ItemModel itemModel = itemService.getItemByIdInCache(itemId);

        if (itemModel == null) {
            throw new BusinessException(EmBusinessError.PARAMETER_VALIDATION_ERROR, "商品信息不存在");
        }
        /**这个地方如果采用token登录模式这个地方是不用放进redis中进行验证user信息的，他这个采用的是oss模型所以需要
         * 将用户信息缓存在redis中提高效率   UserModel userModel = userService.getUserById(userId);*/
        UserModel userModel = userService.getUserByIdInCache(userId);
        if (userModel == null) {
            throw new BusinessException(EmBusinessError.PARAMETER_VALIDATION_ERROR, "用户信息不存在");
        }

        if (amount <= 0 || amount > 99) {
            throw new BusinessException(EmBusinessError.PARAMETER_VALIDATION_ERROR, "数量信息不正确");
        }
        //校验活动信息
        if (promoId != null) {
            //校验对应活动是否存在这个适用商品
            if (!promoId.equals(itemModel.getPromoModel().getId())) {
                throw new BusinessException(EmBusinessError.PARAMETER_VALIDATION_ERROR, "活动信息不正确");
            } else if (itemModel.getPromoModel().getStatus() != 2) {
                //活动是否正在进行中
                throw new BusinessException(EmBusinessError.PARAMETER_VALIDATION_ERROR, "活动还未开始");
            }
        }
    }


    private OrderDO convertFromOrderModel(OrderModel orderModel) {
        if(orderModel == null) return  null;
        OrderDO orderDO = new OrderDO();
        BeanUtils.copyProperties(orderModel,orderDO);
        orderDO.setItemPrice(new Double(String.valueOf(orderModel.getItemPrice())));
        orderDO.setOrderPrice(new Double(String.valueOf(orderModel.getOrderPrice())));
        return  orderDO;
    }





    @Override
    @Transactional(propagation = Propagation.REQUIRES_NEW)
    /*
    这个地方需要保证生成的订单号，必须唯一的
    比如说在上面的createOrder这个创建订单的方法，在进行插入订单表的过程中失败了
    orderDOMapper.insertSelective(orderDO);这个地方加入出现失败了@Transactional回滚了
    那么我们下次生成的订单号非常可能和我们现在生成的订单号是一样的
    因为时间仅仅是年月日 ，后面的6位数完全是由sequence这个表得来的
    只要这个sequence表不更新就表一直一样吗？但是我们希望每次都不一样即使订单失败了也不一样
    所以我们这个地方开启require_new
    */
    public String generateOrderNo(){
    /*
        （感觉这套方案不如UUID）
        订单号有16位
        前8位位时间信息，年月日，中间6位位自增序列，最后2位位分库分表的表位
        这么设计的好处是什么？
        1.前八位是时间信息方便我们在时间上对数据库进行切分。
        比如说我们数据库每天有1亿的增加的话，当我们数据增加到mysql无法承受
        我们就把几个月的信息归档掉，几个月的数据切分出去。这个切分时间点我们就可以通过
        这个Order的id进行切分（比如说小于某年某月某日）
        2.中间6位为自增序列保证数据不重复。不过感觉没有时间序列好
        3.最后两位是00-99，用于做分库分表，做水平拆分用
        比如说我们用userId%100来做分库分表，最终被差分到对应100个库里面的100张表里面

        */
        //订单号16位
        StringBuilder stringBuilder = new StringBuilder();
        //前8位为 年月日
        LocalDateTime now = LocalDateTime.now();
        String nowDate = now.format(DateTimeFormatter.ISO_DATE).replace("-", "");
        stringBuilder.append(nowDate);

        //中间6位为自增序列
        //为了这个自增序列特意设计了一个
        //获取当前sequence
        int sequence = 0;
        SequenceDO sequenceDO = sequenceDOMapper.getSequenceByName("order_info");
        sequence = sequenceDO.getCurrentValue();
        sequenceDO.setCurrentValue(sequenceDO.getCurrentValue() + sequenceDO.getStep());
        sequenceDOMapper.updateByPrimaryKey(sequenceDO);

        //凑足6位
        String sequenceStr = String.valueOf(sequence);
        for (int i = 0; i < 6 - sequenceStr.length(); i++) {
            stringBuilder.append(0);
        }
        stringBuilder.append(sequence);


        //后两位为分库分表位
        stringBuilder.append("00");

        return stringBuilder.toString();
    }
}
